package com.ly.system.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.ly.common.core.bean.R;
import com.ly.common.core.bean.dto.SmsDTO;
import com.ly.common.core.bean.vo.DictVO;
import com.ly.common.core.constant.BaseConstant;
import com.ly.common.core.constant.UsualConstants;
import com.ly.common.core.exception.ServiceException;
import com.ly.common.core.tool.security.config.properties.TokenProperties;
import com.ly.common.core.tool.security.entity.LyUser;
import com.ly.common.core.tool.security.utils.SecurityUtils;
import com.ly.common.core.tool.security.utils.TokenUtils;
import com.ly.common.core.utils.*;
import com.ly.system.mapper.SysPermissionMapper;
import com.ly.system.mapper.SysUserRoleMapper;
import com.ly.system.service.*;
import com.ly.system.system.bean.dto.LoginDTO;
import com.ly.system.system.bean.entity.SysDept;
import com.ly.system.system.bean.entity.SysPermission;
import com.ly.system.system.bean.entity.SysTenant;
import com.ly.system.system.bean.entity.SysUser;
import com.ly.system.system.bean.vo.IndexStatisticVO;
import com.ly.system.system.bean.vo.IndexVisitInfoVO;
import com.ly.system.system.bean.vo.UserInfoVO;
import com.ly.system.util.RandImageUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 用户登录-服务类impl
 *
 * @author luoyun
 * @Date: 2025/5/07 21:00
 * @Note:
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class LoginServiceImpl implements ILoginService, UserDetailsService {

    private final ISysUserService sysUserService;
    private final ISysTenantService sysTenantService;
    private final ISysLogService logService;
    private final RedisUtils redisUtils;
    private final ISysDeptService sysDepartService;
    private final ISysDictService sysDictService;
    private final ISysRoleIndexService sysRoleIndexService;

    private final SysPermissionMapper sysPermissionMapper;
    private final SysUserRoleMapper sysUserRoleMapper;

    private final TokenProperties tokenProperties;

    @Override
    public R login(LoginDTO dto) {
        String username = dto.getUsername();
        String password = dto.getPassword();
        if (checkLoginTimes(username)) {
            return R.fail("登录失败次数过多，请于10分钟后再次登录！");
        }
        // step.1 验证码check
        String captcha = dto.getCaptcha();
        if (StrUtil.isBlank(captcha)) {
            return R.fail("验证码不能为空");
        }
        String lowerCaseCaptcha = captcha.toLowerCase();
        // 加入密钥作为混淆，避免简单的拼接，被外部利用，用户自定义该密钥即可
        String keyPrefix = DigestUtil.md5Hex(dto.getCheckKey() + UsualConstants.signatureSecret);
        String realKey = keyPrefix + lowerCaseCaptcha;
        Object checkCode = redisUtils.get(realKey);
        // 当进入登录页时，有一定几率出现验证码错误 #1714
        if (checkCode == null || !checkCode.toString().equals(lowerCaseCaptcha)) {
            return R.fail("验证码错误");
        }
        // step.2 校验用户是否存在且有效
        SysUser sysUser = sysUserService.getOne(
                new LambdaQueryWrapper<SysUser>()
                        .eq(SysUser::getUsername, username)
        );
        R res = sysUserService.checkUserIsEffective(sysUser);
        if (!res.isSuccess()) {
            return res;
        }
        // step.3 校验用户名或密码是否正确
        String userpassword = CommonUtils.getAesPassWord(username, password, sysUser.getSalt());
        String syspassword = sysUser.getPassword();
        if (!syspassword.equals(userpassword)) {
            addLoginTimes(username);
            return R.fail("用户名或密码错误");
        }
        // step.4  登录成功获取用户信息
        R<UserInfoVO> resR = this.userInfo(sysUser);
        // step.5  登录成功删除验证码
        String failKey = BaseConstant.LOGIN_FAIL + username;
        redisUtils.del(realKey, failKey);
        return resR;
    }

    @Override
    public R phoneLogin(SmsDTO dto) {
        String phone = dto.getMobile();
        if (checkLoginTimes(phone)) {
            return R.fail("登录失败次数过多，请于10分钟后再次登录！");
        }
        String captcha = dto.getCaptcha();
        if (StrUtil.isBlank(captcha)) {
            return R.fail("验证码不能为空");
        }
        String realKey = "";
        // redis读取验证码
        // String realKey = keyPrefix + lowerCaseCaptcha;
        // Object checkCode = redisUtils.get(realKey);
        if (!"000000".equals(captcha)) {
            addLoginTimes(phone);
            return R.fail("验证码错误");
        }
        SysUser sysUser = sysUserService.getUserByPhone(phone);
        R<UserInfoVO> resR = this.userInfo(sysUser);
        // step.5  登录成功删除验证码
        String failKey = BaseConstant.LOGIN_FAIL + phone;
        redisUtils.del(realKey, failKey);
        return resR;
    }

    /**
     * 用户信息
     *
     * @param sysUser
     * @return
     */
    private R<UserInfoVO> userInfo(SysUser sysUser) {
        UserInfoVO vo = new UserInfoVO();
        String username = sysUser.getUsername();
        LyUser lyUser = this.loadUserByUsername(username);
        // springSecurity认证
        Authentication auth = new UsernamePasswordAuthenticationToken(lyUser, null, lyUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(auth);
        String token = TokenUtils.createToken(lyUser);
        vo.setTokenKey(tokenProperties.getHeader());
        String tokenVo = StrUtil.format("{} {}", tokenProperties.getTokenPrefix(), token);
        vo.setToken(tokenVo);
        // 2.设置登录租户
        Integer tenantId = sysUser.getTenantId();
        if (tenantId != null) {
            SysTenant tenant = sysTenantService.getById(tenantId);
            vo.setTenant(tenant);
        }

        // 3.设置登录用户信息
        if (StrUtil.isNotEmpty(username)) {
            // 根据用户名查询用户信息
            String indexUrl = sysRoleIndexService.getIndexUrlByUserName(sysUser.getId());
            // url必须以/ 开头
            if (!indexUrl.startsWith("/")) {
                indexUrl = "/" + indexUrl;
            }
            sysUser.setHomePath(indexUrl);
        }
        vo.setUserInfo(sysUser);
        // 4.设置登录部门
        SysDept departs = sysDepartService.getById(sysUser.getDeptId());
        vo.setDeparts(departs);
        // login接口，在vue3前端下不加载字典数据，vue2下加载字典
        Map<String, List<DictVO>> stringListMap = sysDictService.queryAllDictItems();
        vo.setSysAllDictItems(stringListMap);
        return R.ok(vo);
    }

    @Override
    public R getUserInfo(HttpServletRequest request) {
        String username = BaseUtils.getUsername();
        UserInfoVO vo = new UserInfoVO();
        if (StrUtil.isNotEmpty(username)) {
            // 根据用户名查询用户信息
            SysUser sysUser = sysUserService.getUserByName(username);
            String indexUrl = sysRoleIndexService.getIndexUrlByUserName(sysUser.getId());
            // url必须以/ 开头
            if (!indexUrl.startsWith("/")) {
                indexUrl = "/" + indexUrl;
            }
            sysUser.setHomePath(indexUrl);
            vo.setUserInfo(sysUser);
            Map<String, List<DictVO>> stringListMap = sysDictService.queryAllDictItems();
            vo.setSysAllDictItems(stringListMap);
        }
        return R.ok(vo);
    }

    @Override
    public R logout(HttpServletRequest request) {
        // 用户退出逻辑
        String token = request.getHeader(tokenProperties.getHeader());
        if (StrUtil.isEmpty(token)) {
            return R.fail("退出登录失败！");
        }
        // 删除用户缓存记录
        LyUser lyUser = BaseUtils.getLoginUser();
        if (lyUser != null) {
            log.info(" 用户名:  {},退出成功！ ", lyUser.getUsername());
            TokenUtils.delLoginUser(lyUser.getUuid());
        }
        return R.ok("退出登录成功！");
    }

    @Override
    public R loginInfo() {
        Date dayEnd = DateUtil.date();
        Date dayStart = DateUtil.offsetDay(dayEnd, -7);
        // 获取系统访问记录
        Long totalVisitCount = logService.findTotalVisitCount();
        Long todayVisitCount = logService.findTodayVisitCount(dayStart, dayEnd);
        Long todayIp = logService.findTodayIp(dayStart, dayEnd);
        IndexStatisticVO vo = new IndexStatisticVO(totalVisitCount, todayVisitCount, todayIp);
        return R.ok(vo);
    }

    @Override
    public R<List<IndexVisitInfoVO>> visitInfo() {
        Date dayEnd = DateUtil.date();
        Date dayStart = DateUtil.offsetDay(dayEnd, -7);
        List<IndexVisitInfoVO> list = logService.findVisitCount(dayStart, dayEnd);
        return R.ok(list);
    }

    @Override
    public R getEncryptedString() {
        Map<String, String> map = new HashMap(5);
        map.put("key", AesEncryptUtil.KEY);
        map.put("iv", AesEncryptUtil.IV);
        return R.ok(map);
    }

    @Override
    public R<String> randomImage(String key) {
        try {
            // 生成验证码
            String code = RandomUtil.randomString(UsualConstants.BASE_CHECK_CODES, 4);
            // 存到redis中
            String lowerCaseCode = code.toLowerCase();
            String keyPrefix = DigestUtil.md5Hex(key + UsualConstants.signatureSecret);
            String realKey = keyPrefix + lowerCaseCode;
            redisUtils.removeAll(keyPrefix);
            redisUtils.set(realKey, lowerCaseCode, 60);
            // 返回前端
            String base64 = RandImageUtil.generate(code);
            return R.ok(base64);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return R.fail("获取验证码失败,请检查redis配置!");
        }
    }

    @Override
    public R checkCaptcha(LoginDTO loginDTO) {
        String captcha = loginDTO.getCaptcha();
        String checkKey = loginDTO.getCheckKey();
        if (captcha == null) {
            return R.fail("验证码无效");
        }
        String lowerCaseCaptcha = captcha.toLowerCase();
        String realKey = DigestUtil.md5Hex(lowerCaseCaptcha + checkKey);
        Object checkCode = redisUtils.get(realKey);
        if (checkCode == null || !checkCode.equals(lowerCaseCaptcha)) {
            return R.fail("验证码错误");
        }
        return R.ok();
    }

    @Override
    public R getLoginQrcode() {
        String qrcodeId = BaseConstant.LOGIN_QRCODE_PRE + IdWorker.getIdStr();
        // 定义二维码参数
        Map params = new HashMap(5);
        params.put("qrcodeId", qrcodeId);
        // 存放二维码唯一标识30秒有效
        redisUtils.set(BaseConstant.LOGIN_QRCODE + qrcodeId, qrcodeId, 30);
        return R.ok(params);
    }

    @Override
    public R scanLoginQrcode(String qrcodeId, String token) {
        Object check = redisUtils.get(BaseConstant.LOGIN_QRCODE + qrcodeId);
        if (ObjUtil.isNotEmpty(check)) {
            // 存放token给前台读取
            redisUtils.set(BaseConstant.LOGIN_QRCODE_TOKEN + qrcodeId, token, 60);
        } else {
            return R.fail("二维码已过期,请刷新后重试");
        }
        return R.ok("扫码成功");
    }

    @Override
    public R getQrcodeToken(String qrcodeId) {
        Object token = redisUtils.get(BaseConstant.LOGIN_QRCODE_TOKEN + qrcodeId);
        Map result = new HashMap(5);
        Object qrcodeIdExpire = redisUtils.get(BaseConstant.LOGIN_QRCODE + qrcodeId);
        if (ObjUtil.isEmpty(qrcodeIdExpire)) {
            // 二维码过期通知前台刷新
            result.put("token", "-2");
            return R.ok(result);
        }
        if (ObjUtil.isNotEmpty(token)) {
            result.put("success", true);
            result.put("token", token);
        } else {
            result.put("token", "-1");
        }
        return R.ok(result);
    }

    /**
     * 登录失败超出次数5 返回true
     *
     * @param username
     * @return
     */
    private Boolean checkLoginTimes(String username) {
        String key = BaseConstant.LOGIN_FAIL + username;
        Object failTime = redisUtils.get(key);
        if (failTime != null) {
            Integer val = Integer.parseInt(failTime.toString());
            if (val > 5) {
                return true;
            }
        }
        return false;
    }

    /**
     * 记录登录失败次数
     *
     * @param username
     */
    private void addLoginTimes(String username) {
        String key = BaseConstant.LOGIN_FAIL + username;
        Object failTime = redisUtils.get(key);
        Integer val = 0;
        if (failTime != null) {
            val = Integer.parseInt(failTime.toString());
        }
        // 10分钟，一分钟为60s
        redisUtils.set(key, ++val, 600);
    }

    @Override
    public R<String> sendSms(JSONObject jsonObject) {
        String mobile = jsonObject.get("mobile").toString();
        if (StrUtil.isEmpty(mobile)) {
            return R.fail("手机号不允许为空！");
        }
        String username = BaseUtils.getUsername();
        LambdaQueryWrapper<SysUser> query = new LambdaQueryWrapper<>();
        query.eq(SysUser::getUsername, username).eq(SysUser::getPhone, mobile);
        SysUser user = sysUserService.getOne(query);
        if (null == user) {
            return R.fail("当前登录用户和绑定的手机号不匹配，无法修改密码！");
        }
        String redisKey = BaseConstant.PHONE_REDIS_KEY_PRE + mobile;
        Object object = redisUtils.get(redisKey);
        if (object != null) {
            return R.fail("验证码10分钟内，仍然有效！");
        }
        // 随机数
        String captcha = RandomUtil.randomNumbers(6);
        JSONObject obj = new JSONObject();
        obj.put("code", captcha);
        boolean b = SmsUtils.sendSms();
        if (!b) {
            return R.fail("短信验证码发送失败,请稍后重试");
        }
        // 验证码5分钟内有效
        redisUtils.set(redisKey, captcha, 300);
        return R.ok();
    }

    @Override
    public R<String> smsCheckCaptcha(LoginDTO loginDTO, HttpServletRequest request) {
        String captcha = loginDTO.getCaptcha();
        String checkKey = loginDTO.getCheckKey();
        if (captcha == null) {
            return R.fail("验证码无效");
        }
        String lowerCaseCaptcha = captcha.toLowerCase();
        String realKey = DigestUtil.md5Hex(lowerCaseCaptcha + checkKey + UsualConstants.signatureSecret);
        Object checkCode = redisUtils.get(realKey);
        if (ObjUtil.notEqual(lowerCaseCaptcha, checkCode)) {
            return R.fail("验证码错误");
        }
        String clientIp = NetUtil.getLocalhostStr();
        // 清空短信记录数量
        SmsUtils.clearSendSmsCount(clientIp);
        redisUtils.removeAll(realKey);
        return R.ok();
    }

    @Override
    public LyUser loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = sysUserService.getUserByName(username);
        if (ObjUtil.isNull(user)) {
            log.info("登录用户：{} 不存在.", username);
            throw new ServiceException("用户不存在");
        } else if (!BaseConstant.DEL_FLAG_0.equals(user.getDelFlag())) {
            log.info("登录用户：{} 已被删除.", username);
            throw new ServiceException("用户已被删除");
        } else if (ObjUtil.notEqual(user.getStatus(), 1)) {
            log.info("登录用户：{} 已被停用.", username);
            throw new ServiceException("用户已被停用");
        }
        Set<String> buttonCodeList = new HashSet<>();
        if (SecurityUtils.isAdmin(username)) {
            buttonCodeList.add("admin");
        } else {
            List<SysPermission> allMenuList = sysPermissionMapper.getAuthByUserId(user.getId());
            buttonCodeList = allMenuList.stream()
                    .filter(n -> BaseConstant.MENU_TYPE_2.equals(n.getMenuType()))
                    .map(n -> n.getPerms())
                    .collect(Collectors.toSet());
        }
        LyUser lyUser = BeanUtil.copyProperties(user, LyUser.class);
        lyUser.setButtonPermissions(buttonCodeList)
        // .setMenuPermissions(buttonCodeList)
        // .setRolePermissions(permissionService.getRolePermission(user))
        ;
        return lyUser;
    }
}
